namespace MvvmTools
{
    using System;
    using System.ComponentModel;
    using System.Diagnostics.Contracts;
    using System.Linq.Expressions;
    using JetBrains.Annotations;
    using NotifyPropertyChanged;

    /// <summary>
    /// Extensions for INotifyPropertyChanged. 
    /// </summary>
    public static class NotifyPropertyChangedExtension
    {
        /// <summary>
        /// Subscription to value changed event.
        /// </summary>
        /// <param name="observable"> The observable. </param>
        /// <param name="propertySelector">
        /// The property selector.
        /// </param>
        /// <typeparam name="TProperty">
        /// The property.
        /// </typeparam>
        /// <typeparam name="TObservable"> 
        ///  Anything implementing INotifyPropertyChanged.
        /// </typeparam>
        /// <returns>
        /// Subscription to value changes.
        /// </returns>
        [NotNull] public static IPropertyObservable OnValueChange<TProperty, TObservable>(
            [NotNull] this TObservable observable,
            [NotNull] Expression<Func<TObservable, TProperty>> propertySelector)
            where TObservable : INotifyPropertyChanged
        {
            Contract.Requires<ArgumentNullException>(!Equals(observable, null));
            Contract.Requires<ArgumentNullException>(propertySelector != null);
            Contract.Ensures(Contract.Result<IPropertyObservable>() != null);

            return new RootNotificationListener<TObservable>(observable).OnValueChange(propertySelector);
        }

        /// <summary>
        /// Subscribes to either of two events, namely, an observable object that is associated 
        /// with the specified property was changed or it provides new data.
        /// </summary>
        /// <param name="observable"> The observable. </param>
        /// <param name="propertySelector">
        /// A lambda expression which returns the reference to a property that has to be observed.
        /// </param>
        /// <typeparam name="TElement">
        /// The type of an element within <see cref="IObservable{TElement}"/> source.
        /// </typeparam>
        /// <typeparam name="TObservable"> 
        ///  Anything implementing INotifyPropertyChanged.
        /// </typeparam>
        /// <returns>
        /// The <see cref="IPropertyObservable"/> subscription.
        /// </returns>
        [NotNull] public static IPropertyObservable OnObservableChange<TElement, TObservable>(
            [NotNull] this TObservable observable,
            [NotNull] Expression<Func<TObservable, IObservable<TElement>>> propertySelector)
            where TObservable : INotifyPropertyChanged
        {
            Contract.Requires<ArgumentNullException>(!Equals(observable, null));
            Contract.Requires<ArgumentNullException>(propertySelector != null);
            Contract.Ensures(Contract.Result<IPropertyObservable>() != null);

            return new RootNotificationListener<TObservable>(observable).OnObservableChange(propertySelector);
        }

        /// <summary>
        /// Subscription to value changed event for all changes.
        /// </summary>
        /// <param name="observable"> The observable. </param>
        /// <typeparam name="TObservable"> 
        ///  Anything implementing INotifyPropertyChanged.
        /// </typeparam>
        /// <returns>
        /// Subscription to value changes.
        /// </returns>
        [NotNull] public static IPropertyObservable OnAnyChanges<TObservable>([NotNull] this TObservable observable)
            where TObservable : INotifyPropertyChanged
        {
            Contract.Requires<ArgumentNullException>(!Equals(observable, null));
            Contract.Ensures(Contract.Result<IPropertyObservable>() != null);
            
            return new RootNotificationListener<TObservable>(observable).OnAnyChanges();
        }

        /// <summary>
        /// Subscribes to property changes.
        /// </summary>
        /// <param name="observable">
        /// The observable.
        /// </param>
        /// <param name="propertySelector">
        /// The property selector.
        /// </param>
        /// <typeparam name="TProperty">
        /// The property.
        /// </typeparam>
        /// <typeparam name="TObservable">
        /// Anything implementing INotifyPropertyChanged.
        /// </typeparam>
        /// <returns>
        /// Subscription to property changes.
        /// </returns>
        [NotNull] public static IChangeListener<TProperty> OnChange<TProperty, TObservable>(
            [NotNull] this TObservable observable,
            [NotNull] Expression<Func<TObservable, TProperty>> propertySelector)
            where TProperty : INotifyPropertyChanged
            where TObservable : INotifyPropertyChanged
        {
            Contract.Requires<ArgumentNullException>(!Equals(observable, null));
            Contract.Requires<ArgumentNullException>(propertySelector != null);
            Contract.Ensures(Contract.Result<IChangeListener<TProperty>>() != null);

            return new RootNotificationListener<TObservable>(observable).OnChange(propertySelector);
        }

        /// <summary>
        /// Subscribes to property changed event and initializes with default value.
        /// </summary>
        /// <param name="parent">
        /// The parent.
        /// </param>
        /// <param name="propertySelector">
        /// The property selector.
        /// </param>
        /// <param name="setter">
        /// The setter.
        /// </param>
        /// <typeparam name="TParent">
        /// The type of parent.
        /// </typeparam>
        /// <typeparam name="TProperty">
        /// The type of property.
        /// </typeparam>
        /// <returns>
        /// The observer's interface that enables resources to be disposed.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="parent"/> or <paramref name="propertySelector"/> argument is <c>null</c>.
        /// </exception>
        /// <example>
        /// Using MapTo method.
        /// <code>
        /// class Model : NotificationModel
        /// {
        ///     private bool someFlag;
        /// 
        ///     public bool SomeFlag
        ///     { 
        ///         get { return someFlag; }
        ///         set { UpdateProperty(() =&gt; SomeFlag, ref someFlag, value); }
        ///     }
        /// }
        /// 
        /// class ViewModel : NotificationModel
        /// {
        ///     public ViewModel(Model model)
        ///     {
        ///         Anchors.Add(model.MapTo(m =&gt; m.SomeFlag, value =&gt; AnotherFlag = value));
        ///     }
        /// 
        ///     public bool AnotherFlag { get; set; }
        /// }
        /// </code>
        /// </example>
        [NotNull] public static IDisposable MapTo<TParent, TProperty>(
            [NotNull] this TParent parent, 
            [NotNull] Expression<Func<TParent, TProperty>> propertySelector,
            [NotNull] Action<TProperty> setter)
            where TParent : INotifyPropertyChanged
        {
            Contract.Requires<ArgumentNullException>(!Equals(parent, null));
            Contract.Requires<ArgumentNullException>(propertySelector != null);
            Contract.Requires<ArgumentNullException>(setter != null);
            Contract.Ensures(Contract.Result<IDisposable>() != null);

            var valueGetter = propertySelector.Compile();
            setter(valueGetter(parent));
            return parent.OnValueChange(propertySelector).Subscribe(new Observer<PropertyChange>(e => setter(valueGetter(parent))));
        }

        private class Observer<T> : IObserver<T>
        {
            private readonly Action<T> _action;

            public Observer(Action<T> action)
            {
                _action = action;
            }

            public void OnNext(T value)
            {
                _action(value);
            }

            public void OnError(Exception error)
            {
            }

            public void OnCompleted()
            {
            }
        }
    }
}